package by.brest.bspc.reporter.extractors;

import by.brest.bspc.domain.Grade;
import by.brest.bspc.domain.Specialty;
import by.brest.bspc.domain.Student;
import by.brest.bspc.reporter.Faculties;
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.microsoft.OfficeParser;
import org.apache.tika.sax.BodyContentHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import static by.brest.bspc.reporter.ReporterConstants.CLASS_KEY;
import static by.brest.bspc.reporter.ReporterConstants.COLUMNS_KEYS;
import static by.brest.bspc.reporter.ReporterConstants.COLUMNS_KEYS_EXAM;
import static by.brest.bspc.reporter.ReporterConstants.DOC_EXTENSION;
import static by.brest.bspc.reporter.ReporterConstants.DOPK;
import static by.brest.bspc.reporter.ReporterConstants.EXAM_ELEMENT;
import static by.brest.bspc.reporter.ReporterConstants.KEY_COLUMNS;
import static by.brest.bspc.reporter.ReporterConstants.KEY_COLUMNS_EXAM;
import static by.brest.bspc.reporter.ReporterConstants.PRACTICE_ELEMENT;
import static by.brest.bspc.reporter.ReporterConstants.SEMESTER_1ST_KEYS;
import static by.brest.bspc.reporter.ReporterConstants.SEMESTER_1_ST;
import static by.brest.bspc.reporter.ReporterConstants.SEMESTER_2ND_KEYS;
import static by.brest.bspc.reporter.ReporterConstants.SEMESTER_2_ND;
import static by.brest.bspc.reporter.ReporterConstants.SEMESTER_ELEMENT;
import static by.brest.bspc.reporter.ReporterConstants.SPECIALTY_KEY;
import static by.brest.bspc.reporter.ReporterConstants.STATUS_KEY;
import static by.brest.bspc.reporter.ReporterConstants.STUDY_B_KEY_WITHOUT_SPACES;
import static by.brest.bspc.reporter.ReporterConstants.STUDY_B_KEY_WITH_SPACES;
import static by.brest.bspc.reporter.ReporterConstants.STUDY_PL_KEY_WITHOUT_SPACES;
import static by.brest.bspc.reporter.ReporterConstants.STUDY_PL_KEY_WITH_SPACES;
import static by.brest.bspc.reporter.ReporterConstants.STUDY_P_KEY_WITHOUT_SPACES;
import static by.brest.bspc.reporter.ReporterConstants.STUDY_P_KEY_WITH_SPACES;
import static by.brest.bspc.reporter.ReporterConstants.XLS_EXTENSION;

/**
 * Extract the data about students and their workload.
 *
 * @date: 8/19/13 7:24 PM
 * @author: natallia
 */

public class StudentsExtractor implements WorkLoadExtractor {
    private static final Logger LOG = LoggerFactory.getLogger(StudentsExtractor.class);

    private Iterator studentsIterator;
    private Iterator workloadIterator;
    private Map<String, List<Byte>> columns;
    private Specialty specialty;
    private int classNumber;

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Student> extractStudentsWorkload(File directoryWithStudentsFiles, File directoryWithWorkloads,
                                                 String groupName) {
        LOG.debug(">> Students directory: " + directoryWithStudentsFiles + "; workloads directory: " + directoryWithWorkloads
                + "; for group: " + groupName);

        List<Grade> grades = createGradeList(directoryWithWorkloads, groupName);

        LOG.debug(">> Students workload: " + grades);

        List<Student> students = new ArrayList<>();
        String previousLine = "";

        studentsIterator = parse(new File(directoryWithStudentsFiles, groupName + DOC_EXTENSION), false).iterator();
        while (studentsIterator.hasNext()) {
            String[] lines = ((String[]) studentsIterator.next());
            String line = lines.length > 0 ? lines[0] : "";

            LOG.debug(">> Current line: " + line);

            if (line.contains(SPECIALTY_KEY)) {
                specialty = new Specialty();
                specialty.setCode(line.substring(line.indexOf(SPECIALTY_KEY) + SPECIALTY_KEY.length(),
                        line.indexOf(0x00AB)).trim());
                specialty.setName(line.substring(line.indexOf(0x00AB) + 1, line.indexOf(0x00BB)).trim());

                LOG.debug(">> Created specialty: " + specialty);
            } else if (line.contains(CLASS_KEY)) {
                classNumber = Integer.parseInt(line.substring(line.indexOf(CLASS_KEY) - 2, line.indexOf(CLASS_KEY) - 1).trim());

                LOG.debug(">> Class number: " + classNumber);
            } else if (line.trim().equalsIgnoreCase(STUDY_P_KEY_WITHOUT_SPACES) || line.trim().equalsIgnoreCase(STUDY_B_KEY_WITHOUT_SPACES) || line.trim().equalsIgnoreCase(STUDY_PL_KEY_WITHOUT_SPACES)) {
                students.add(createStudent(previousLine.trim(), line.trim(), groupName, grades));
            } else if (line.contains(STUDY_P_KEY_WITH_SPACES) || line.contains(STUDY_B_KEY_WITH_SPACES) || line.contains(STUDY_PL_KEY_WITH_SPACES)) {
                String[] words = line.split(" ");
                for (int i = 0; i < words.length; i++) {
                    if (words[i].equalsIgnoreCase(STUDY_P_KEY_WITHOUT_SPACES) ||
                            words[i].equalsIgnoreCase(STUDY_B_KEY_WITHOUT_SPACES) ||
                            words[i].equalsIgnoreCase(STUDY_PL_KEY_WITHOUT_SPACES)) {

                        students.add(createStudent(words[i - 3] + " " + words[i - 2] + " " + words[i - 1], words[i],
                                groupName, grades));
                    }
                }
            }
            previousLine = line;
        }

        return students;
    }

    /**
     * Creates and initializes a student.
     *
     * @param studentName student name.
     * @param study       study type.
     * @param groupName   group name.
     * @param grades      students workload.
     * @return created and initialized student.
     */
    private Student createStudent(String studentName, String study, String groupName, List<Grade> grades) {
        LOG.debug(">> Student name: " + studentName + "; study type: " + study + "; group name: " + groupName
                + "; workload: " + grades);

        // Recalculate semester number.
        for (Grade grade : grades) {
            int semester = grade.getSemesterNumber();
            if (semester == 1) {
                grade.setSemesterNumber(classNumber * 2 - 1);
            } else if (semester == 2) {
                grade.setSemesterNumber(classNumber * 2);
            }
        }

        // Init student
        Student student = new Student();
        student.setId(UUID.randomUUID().toString().replace("-", "").substring(0, 24));
        student.setName(studentName);
        student.setStudy(study);

        for (Faculties faculty : Faculties.values()) {
            if (faculty.getKey().equals(groupName.charAt(0))) {
                student.setFaculty(faculty.getName());
            }
        }

        student.setSpecialty(specialty);
        student.setClassNumber(classNumber);
        student.setGroupName(groupName);
        student.setStatus(STATUS_KEY);
        student.setGradeList(grades);

        LOG.debug(">> Created student: " + student);

        return student;
    }

    /**
     * Parses the data from .xls or .doc file to the map of the strings.
     *
     * @param file       excel file that need to parse.
     * @param isWorkload flag of type of parser.
     * @return map of the strings.
     */
    private List<String[]> parse(File file, boolean isWorkload) {
        LOG.debug(">> Input file: " + file.getAbsolutePath() + "; workload parser: " + isWorkload);

        String[] lines = extractData(file).split("\n");

        List<String[]> parsedData = new LinkedList<>();
        for (String line : lines) {
            LOG.debug(">> Current line: " + line);
            parsedData.add(line.split(isWorkload ? "\t" : "\r"));
        }

        return parsedData;
    }

    /**
     * Extracts the data from an excel file.
     *
     * @param file excel file.
     * @return the string representation of the data.
     */
    private String extractData(File file) {
        LOG.debug(">> Input file: " + file.getAbsolutePath());

        Metadata metadata = new Metadata();
        ContentHandler contentHandler = new BodyContentHandler();

        try (
                InputStream input = new FileInputStream(file)
        ) {
            new OfficeParser().parse(input, contentHandler, metadata);
        } catch (SAXException | IOException | TikaException e) {
            LOG.error("Can`t extract data from excel file " + file.getAbsolutePath() + ".");
        }

        LOG.debug(">> Extracted content: " + contentHandler);

        return contentHandler.toString();
    }

    /**
     * Creates grade list for group
     *
     * @param directoryWithWorkloads a directory with students workload.
     * @param groupName              groupName.
     * @return created grade list.
     */
    private List<Grade> createGradeList(File directoryWithWorkloads, String groupName) {
        LOG.debug(">> Workloads directory: " + directoryWithWorkloads + "; for group: " + groupName);

        List<Grade> gradeList = new LinkedList<>();
        List<Integer> semesters;

        workloadIterator = parse(new File(directoryWithWorkloads, groupName + XLS_EXTENSION), true).iterator();
        analysisTableCaption();

        String[] line = (String[]) workloadIterator.next();
        while (line.length != 0) {
            Grade grade = createGrade(gradeList, line);

            LOG.debug(">> Created grade: " + grade);

            if (grade != null) {
                semesters = getSemesters(line);

                LOG.debug(">> Semesters: " + semesters);

                boolean semester2ndIsExam = false;
                String gradeType = grade.getGradeType();
                if (gradeType.contains(EXAM_ELEMENT)) {
                    if (semesters.size() == 1) {
                        grade.setGradeType(EXAM_ELEMENT);
                    } else {
                        if (grade.getGradeType().contains("_3")) {
                            grade.setGradeType(SEMESTER_ELEMENT);
                        } else {
                            grade.setGradeType(EXAM_ELEMENT);
                        }
                        semester2ndIsExam = true;
                    }
                }

                if (!semesters.isEmpty()) {
                    grade.setSemesterNumber(semesters.get(0));
                    gradeList.add(grade);
                    if (grade.getGradeType().equalsIgnoreCase(EXAM_ELEMENT)) {
                        Grade semesterGrade = new Grade(grade);
                        semesterGrade.setGradeType(SEMESTER_ELEMENT);
                        gradeList.add(semesterGrade);
                    }
                }

                if (semesters.size() == 2) {
                    LOG.debug("Create grade with discipline name " + grade.getDisciplineName()
                            + " and teacher name " + grade.getTeacherName() + ".");

                    Grade grade_2nd_semester = new Grade(grade);
                    grade_2nd_semester.setSemesterNumber(semesters.get(1));
                    if (semester2ndIsExam) {
                        grade_2nd_semester.setGradeType(EXAM_ELEMENT);
                    }

                    LOG.debug(">> Add grade to grade list: " + grade_2nd_semester);

                    gradeList.add(grade_2nd_semester);

                    if (semester2ndIsExam) {
                        Grade grade_2nd_semester_semester = new Grade(grade_2nd_semester);
                        grade_2nd_semester_semester.setGradeType(SEMESTER_ELEMENT);
                        gradeList.add(grade_2nd_semester_semester);
                    }
                }
            }
            line = (String[]) workloadIterator.next();
        }
        LOG.debug(">> Grade list: " + gradeList);

        return gradeList;
    }

    /**
     * Analyzes the table caption and gets the numbers of the fields, in which stored the data,
     * that corresponds semesters, discipline name, teacher name.
     */
    private void analysisTableCaption() {
        // Skip unnecessary lines.
        for (int i = 0; i < 3; i++) {
            workloadIterator.next();
        }

        columns = new HashMap<>();
        columns.put(KEY_COLUMNS, new ArrayList<Byte>());
        columns.put(KEY_COLUMNS_EXAM, new ArrayList<Byte>());
        columns.put(SEMESTER_1_ST, new ArrayList<Byte>());
        columns.put(SEMESTER_2_ND, new ArrayList<Byte>());

        String[] line = (String[]) workloadIterator.next(); // First line of the table caption.
        for (byte i = 0; i < line.length; i++) {
            if (COLUMNS_KEYS.contains(line[i])) {
                LOG.debug(">> Column name: " + line[i].trim() + "; column number: " + i);

                columns.get(KEY_COLUMNS).add(i);
            }
            if (COLUMNS_KEYS_EXAM.contains(line[i])) {
                LOG.debug(">> Column name: " + line[i].trim() + "; column number: " + i);

                columns.get(KEY_COLUMNS_EXAM).add(i);
            }
        }

        line = (String[]) workloadIterator.next(); // Second line of the table caption.
        for (byte i = 0; i < line.length; i++) {
            if (SEMESTER_1ST_KEYS.contains(line[i])) {
                LOG.debug(">> Column name: " + line[i].trim() + "; column number: " + i);

                columns.get(SEMESTER_1_ST).add(i);
            }
            if (SEMESTER_2ND_KEYS.contains(line[i])) {
                LOG.debug(">> Column name: " + line[i].trim() + "; column number: " + i);

                columns.get(SEMESTER_2_ND).add(i);
            }
            if (COLUMNS_KEYS_EXAM.contains(line[i])) {
                LOG.debug(">> Column name: " + line[i].trim() + "; column number: " + i);

                columns.get(KEY_COLUMNS_EXAM).add(i);
            }
        }

        LOG.debug(">> Columns: " + columns);
    }

    /**
     * Create grade from current line.
     *
     * @param gradeList a grade list.
     * @param line      a current line.
     * @return created grade.
     */
    private Grade createGrade(List<Grade> gradeList, String[] line) {
        LOG.debug(">> Grade list: " + gradeList);

        String disciplineName = line[columns.get(KEY_COLUMNS).get(0)];
        String teacherName = line[columns.get(KEY_COLUMNS).get(1)];

        LOG.debug(">> Discipline name: " + disciplineName + "; teacher name: " + teacherName);

        if (!disciplineName.trim().startsWith(DOPK)) {
            for (Grade grade : gradeList) {
                if (grade.getDisciplineName().equalsIgnoreCase(disciplineName) &&
                        grade.getTeacherName().equalsIgnoreCase(teacherName)) {

                    LOG.debug("Grade with discipline name " + disciplineName + " and teacher name " + teacherName
                            + " is exists.");

                    return null;
                }
            }

            LOG.debug("Create grade with discipline name " + disciplineName + " and teacher name " + teacherName + ".");

            Grade grade = new Grade();
            grade.setDisciplineName(disciplineName);
            grade.setTeacherName(teacherName);
            if (disciplineName.contains("Практика") || disciplineName.contains("практика")) {
                grade.setGradeType(PRACTICE_ELEMENT);
            } else if (line.length >= (columns.get(KEY_COLUMNS_EXAM).get(0) + 1)
                    && !line[columns.get(KEY_COLUMNS_EXAM).get(0)].isEmpty()
                    && line[columns.get(KEY_COLUMNS_EXAM).get(0)].contains("экзамен")) {
                grade.setGradeType(EXAM_ELEMENT + "_" + line[columns.get(KEY_COLUMNS_EXAM).get(1)]);
            } else {
                grade.setGradeType(SEMESTER_ELEMENT);
            }

            return grade;
        }
        return null;
    }

    /**
     * Determines semesters in which teach a discipline.
     *
     * @param line line of an Excel file, that corresponds a discipline.
     */
    private List<Integer> getSemesters(String[] line) {
        List<Integer> semesters = new LinkedList<>();

        for (int i = 0; i < columns.get(SEMESTER_1_ST).size(); i++) {
            if ((i < columns.get(SEMESTER_1_ST).size())
                    && (!line[columns.get(SEMESTER_1_ST).get(i)].equalsIgnoreCase("")) && (!semesters.contains(1))) {
                semesters.add(1);
            }
        }

        for (int i = 0; i < columns.get(SEMESTER_2_ND).size(); i++) {
            if ((i < columns.get(SEMESTER_2_ND).size())
                    && (!line[columns.get(SEMESTER_2_ND).get(i)].equalsIgnoreCase("")) && (!semesters.contains(2))) {
                semesters.add(2);
            }
        }

        return semesters;
    }
}
